using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;

using STuple = System.Tuple;

namespace ReactiveLang
{
    using Util;

    partial class Expression
    {
        public static IExpression<T> Empty<T>()
        {
            return CreateWithDisposable<T>(_ => Disposable.Empty);
        }
        public static IExpression<T> Constant<T>(T value)
        {
            return CreateWithDisposable<T>(o =>
            {
                o.OnValue(value);
                return Disposable.Empty;
            });
        }
        public static IExpression<TResult> Unary<TSource, TResult>(this IExpression<TSource> source, Func<TSource, TResult> selector)
        {
            return CreateWithDisposable<TResult>(o => source.ActivateDerived(o, x => o.OnValue(selector(x))));
        }
        public static IExpression<TResult> Binary<TLeft, TRight, TResult>(this IExpression<TLeft> left, IExpression<TRight> right, Func<TLeft, TRight, TResult> selector)
        {
            return CreateWithDisposable<TResult>(o =>
            {
                var latestLeft = left.Latest(o);
                var latestRight = right.Latest(o);

                Action onChanged = delegate
                {
                    if (latestLeft.HasValue && latestRight.HasValue)
                        o.OnValue(selector(latestLeft.Value, latestRight.Value));
                };
                latestLeft.Activate(_ => onChanged());
                latestRight.Activate(_ => onChanged());

                return latestLeft.Concat(latestRight);
            });
        }

        static IExpression<TResult> Combine<TSource, TResult>(Func<Func<int, TSource>, Func<TResult>> resultSelector, params IExpression<TSource>[] sources)
        {
            Contract.Requires<ArgumentNullException>(resultSelector != null, "resultSelector");
            Contract.Requires<ArgumentNullException>(sources != null, "sources");

            var src = sources.ToArray();
            if (src.Length == 0)
                return CreateWithDisposable<TResult>(o =>
                {
                    var selector = resultSelector(_ => { throw new ArgumentOutOfRangeException(); });
                    o.OnValue(selector());
                    return Disposable.Empty;
                });

            return Create<TResult>(o =>
            {
                var latest = src.Select(x => x.Latest(o)).ToArray();
                var selector = resultSelector(i => latest[i].Value);
                bool allHaveValue = false;
                foreach(var value in latest)
                {
                    value.Activate(_ =>
                    {
                        if (!allHaveValue)
                            allHaveValue = latest.All(v => v.HasValue);
                        if (allHaveValue)
                            o.OnValue(selector());
                    });
                }

                return delegate
                {
                    foreach(var value in latest)
                        value.Dispose();
                };
            });

        }
        static IExpression<TResult> Combine<TSource, TResult>(Func<Func<int, TSource>, TResult> resultSelector, params IExpression<TSource>[] sources)
        {
            Contract.Requires(resultSelector != null);
            return Combine<TSource, TResult>(f => () => resultSelector(f), sources);
        }
        public static IExpression<T[]> ToArray<T>(params IExpression<T>[] sources)
        {
            Contract.Requires<ArgumentNullException>(sources != null, "sources");
            if (sources.Length == 0)
                return Constant(CollectionUtil.EmptyArray<T>());

            Func<Func<int, T>, T[]> resultSelector = f =>
            {
                T[] result = new T[sources.Length];
                for(int i = 0; i < sources.Length; i++)
                    result[i] = f(i);
                return result;
            };
            return Combine<T, T[]>(resultSelector, sources);
        }
        public static IExpression<TResult> Apply<TSource, TResult>(this IExpression<TSource> source, Func<TSource, IExpression<TResult>> func)
        {
            Contract.Requires<ArgumentNullException>(source != null, "source");
            Contract.Requires<ArgumentNullException>(func != null, "func");
            return Create<TResult>(o =>
            {
                var resultHandle = Disposable.Empty;
                var srcHandle = source.ActivateDerived(o, x =>
                {
                    resultHandle.Dispose();
                    var result = func(x);
                    if (result != null)
                        resultHandle = result.Activate(o);
                });
                return delegate
                {
                    resultHandle.Dispose();
                    srcHandle.Dispose();
                };
            });
        }

        #region Tuple

        [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)]
        public static IExpression<object> Box<T>(this IExpression<T> expr)
        {
            return expr.Unary(x => x as object);
        }

        public static IExpression<Tuple<T1>> Tuple<T1>(IExpression<T1> item1)
        {
            Contract.Requires<ArgumentNullException>(item1 != null, "item1");
            return Expression.CreateWithDisposable<Tuple<T1>>(o => item1.ActivateDerived(o, x => o.OnValue(STuple.Create(x))));
        }
        public static IExpression<Tuple<T1, T2>> Tuple<T1, T2>(IExpression<T1> item1, IExpression<T2> item2)
        {
            Contract.Requires<ArgumentNullException>(item1 != null, "item1");
            Contract.Requires<ArgumentNullException>(item2 != null, "item2");
            return item1.Binary(item2, STuple.Create<T1, T2>);
        }
        public static IExpression<Tuple<T1, T2, T3>> Tuple<T1, T2, T3>(IExpression<T1> item1, IExpression<T2> item2, IExpression<T3> item3)
        {
            Contract.Requires<ArgumentNullException>(item1 != null, "item1");
            Contract.Requires<ArgumentNullException>(item2 != null, "item2");
            Contract.Requires<ArgumentNullException>(item3 != null, "item3");
            return Tuple(item1, item2).Binary(item3, (ab, c) => STuple.Create(ab.Item1, ab.Item2, c));
        }
        public static IExpression<Tuple<T1, T2, T3, T4>> Tuple<T1, T2, T3, T4>(IExpression<T1> item1, IExpression<T2> item2, IExpression<T3> item3, 
            IExpression<T3> item4)
        {
            Contract.Requires<ArgumentNullException>(item1 != null, "item1");
            Contract.Requires<ArgumentNullException>(item2 != null, "item2");
            Contract.Requires<ArgumentNullException>(item3 != null, "item3");
            Contract.Requires<ArgumentNullException>(item4 != null, "item4");
            return Combine(f => STuple.Create((T1) f(0), (T2) f(1), (T3) f(2), (T4) f(3)),
                item1.Box(), item2.Box(), item3.Box(), item4.Box());
        }
        public static IExpression<Tuple<T1, T2, T3, T4, T5>> Tuple<T1, T2, T3, T4, T5>(IExpression<T1> item1, IExpression<T2> item2, IExpression<T3> item3, 
            IExpression<T3> item4, IExpression<T5> item5)
        {
            Contract.Requires<ArgumentNullException>(item1 != null, "item1");
            Contract.Requires<ArgumentNullException>(item2 != null, "item2");
            Contract.Requires<ArgumentNullException>(item3 != null, "item3");
            Contract.Requires<ArgumentNullException>(item4 != null, "item4");
            Contract.Requires<ArgumentNullException>(item5 != null, "item5");
            return Combine(f => STuple.Create((T1)f(0), (T2)f(1), (T3)f(2), (T4)f(3), (T5)f(4)),
                item1.Box(), item2.Box(), item3.Box(), item4.Box(), item5.Box());
        }
        public static IExpression<Tuple<T1, T2, T3, T4, T5, T6>> Tuple<T1, T2, T3, T4, T5, T6>(IExpression<T1> item1, IExpression<T2> item2, IExpression<T3> item3,
            IExpression<T3> item4, IExpression<T5> item5, IExpression<T6> item6)
        {
            Contract.Requires<ArgumentNullException>(item1 != null, "item1");
            Contract.Requires<ArgumentNullException>(item2 != null, "item2");
            Contract.Requires<ArgumentNullException>(item3 != null, "item3");
            Contract.Requires<ArgumentNullException>(item4 != null, "item4");
            Contract.Requires<ArgumentNullException>(item5 != null, "item5");
            Contract.Requires<ArgumentNullException>(item6 != null, "item6");
            return Combine(f => STuple.Create((T1)f(0), (T2)f(1), (T3)f(2), (T4)f(3), (T5)f(4), (T6)f(5)),
                item1.Box(), item2.Box(), item3.Box(), item4.Box(), item5.Box(), item6.Box());
        }
        public static IExpression<Tuple<T1, T2, T3, T4, T5, T6, T7>> Tuple<T1, T2, T3, T4, T5, T6, T7>(IExpression<T1> item1, IExpression<T2> item2, IExpression<T3> item3,
            IExpression<T3> item4, IExpression<T5> item5, IExpression<T6> item6, IExpression<T7> item7)
        {
            Contract.Requires<ArgumentNullException>(item1 != null, "item1");
            Contract.Requires<ArgumentNullException>(item2 != null, "item2");
            Contract.Requires<ArgumentNullException>(item3 != null, "item3");
            Contract.Requires<ArgumentNullException>(item4 != null, "item4");
            Contract.Requires<ArgumentNullException>(item5 != null, "item5");
            Contract.Requires<ArgumentNullException>(item6 != null, "item6");
            Contract.Requires<ArgumentNullException>(item7 != null, "item7");
            return Combine(f => STuple.Create((T1)f(0), (T2)f(1), (T3)f(2), (T4)f(3), (T5)f(4), (T6)f(5), (T7)f(6)),
                item1.Box(), item2.Box(), item3.Box(), item4.Box(), item5.Box(), item6.Box(), item7.Box());
        }
        public static IExpression<Tuple<T1, T2, T3, T4, T5, T6, T7, Tuple<TRest>>> Tuple<T1, T2, T3, T4, T5, T6, T7, TRest>(IExpression<T1> item1, IExpression<T2> item2, IExpression<T3> item3,
            IExpression<T3> item4, IExpression<T5> item5, IExpression<T6> item6, IExpression<T7> item7, IExpression<TRest> item8)
        {
            Contract.Requires<ArgumentNullException>(item1 != null, "item1");
            Contract.Requires<ArgumentNullException>(item2 != null, "item2");
            Contract.Requires<ArgumentNullException>(item3 != null, "item3");
            Contract.Requires<ArgumentNullException>(item4 != null, "item4");
            Contract.Requires<ArgumentNullException>(item5 != null, "item5");
            Contract.Requires<ArgumentNullException>(item6 != null, "item6");
            Contract.Requires<ArgumentNullException>(item7 != null, "item7");
            Contract.Requires<ArgumentNullException>(item8 != null, "item8");
            return Combine(f => STuple.Create((T1)f(0), (T2)f(1), (T3)f(2), (T4)f(3), (T5)f(4), (T6)f(5), (T7)f(6), (TRest)f(7)),
                item1.Box(), item2.Box(), item3.Box(), item4.Box(), item5.Box(), item6.Box(), item7.Box(), item8.Box());
        }

        #endregion

        #region Event

        class EventObserverHandle<TEventArgs> : IDisposable
        {
            IExpressionObserver<Tuple<object, TEventArgs>> _observer;
            Action _removeHandler;

            public EventObserverHandle(object target, EventInfo @event, IExpressionObserver<Tuple<object, TEventArgs>> observer)
            {
                Contract.Requires(@event != null);
                Contract.Requires(observer != null);
                _observer = observer;
                var handler = Delegate.CreateDelegate(@event.EventHandlerType, this, "Handle");
                @event.AddEventHandler(target, handler);
                _removeHandler = () => @event.RemoveEventHandler(target, handler);
            }
            public void Dispose()
            {
                _removeHandler();
            }

            public void Handle(object sender, TEventArgs e)
            {
                _observer.OnValue(STuple.Create(sender, e));
            }
        }

        public static IExpression<Tuple<object, TEventArgs>> Event<TEventArgs>(object target, EventInfo @event)
        {
            Contract.Requires(@event != null && @event.DeclaringType != null);
            Contract.Requires(@event.GetAddMethod() != null && @event.GetRemoveMethod() != null);
            Contract.Requires((target == null) == @event.GetAddMethod().IsStatic);
            Contract.Requires(target == null || @event.DeclaringType.IsAssignableFrom(target.GetType()));

            return CreateWithDisposable<Tuple<object, TEventArgs>>(o => new EventObserverHandle<TEventArgs>(target, @event, o));
        }
        static IExpression<Unit> EventAsUnit<TEventArgs>(object target, EventInfo @event)
        {
            return Event<TEventArgs>(target, @event).AsUnit();
        }
        public static IExpression<Unit> Event(object target, EventInfo @event)
        {
            var invoke = @event.EventHandlerType.GetMethod("Invoke");
            return (IExpression<Unit>) typeof(Expression).GetMethod("EventAsUnit", BindingFlags.NonPublic | BindingFlags.Static)
                .MakeGenericMethod(invoke.GetParameters()[1].ParameterType)
                .Invoke(null, new object[] { target, @event });
        }

        #endregion

        #region Member

        public delegate IExpression<T> MemberReader<T>(IExpression<object>[] arguments, State.ChangeNotifierOptions options = null);
        public static MemberReader<T> PrepareMember<T>(MemberInfo member, Func<IList<object>, T> valueSelector)
        {
            Contract.Requires<ArgumentNullException>(member != null, "member");
            Contract.Requires<ArgumentNullException>(valueSelector != null, "valueSelector");

            var notifier = State.ChangeNotifications.TryGetNotifier(member);

            return (arguments, options) =>
            {
                Contract.Requires<ArgumentNullException>(arguments != null, "arguments");
                int expectedArgCount = member.GetArgumentCount();
                if (expectedArgCount != arguments.Length)
                    throw new ArgumentException("Invalid argument count. Expected: {0}, actual: {1}".InvariantFormat(expectedArgCount, arguments.Length), "arguments");

                var args = Combine<object, IList<object>>(f =>
                {
                    var array = new object[arguments.Length];
                    var collection = Array.AsReadOnly(array);
                    return () =>
                    {
                        for (int i = 0; i < array.Length; i++)
                            array[i] = f(i);
                        return collection;
                    };
                }, arguments);

                if (notifier != null)
                {
                    args = from a in args
                           from _ in notifier(a, options).MakeWeak().WithInitial()
                           select a;
                }

                return args.Unary(valueSelector);
            };
        }
        public static IExpression<T> Member<T>(MemberInfo member, IExpression<object>[] arguments, Func<IList<object>, T> valueSelector, State.ChangeNotifierOptions options = null)
        {
            Contract.Requires<ArgumentNullException>(member != null, "member");
            Contract.Requires<ArgumentNullException>(arguments != null, "arguments");
            Contract.Requires<ArgumentNullException>(valueSelector != null, "valueSelector");

            return PrepareMember(member, valueSelector)(arguments, options);
        }

        #endregion

        public static IExpression<TValue> Assign<TValue>(this IAssignableExpression<TValue> state, IExpression<TValue> value)
        {
            Contract.Requires<ArgumentNullException>(state != null, "target");
            Contract.Requires<ArgumentNullException>(value != null, "value");

            return Create<TValue>(o =>
            {
                bool hasExternal = false;
                var lastExternal = default(TValue);
                bool assigning = false;
                var targetSubscription = state.ActivateDerived(o, x =>
                {
                    if (!assigning)
                    {
                        hasExternal = true;
                        lastExternal = x;
                    }
                });

                var sync = new Sync();
                Action<TValue> assign = x => sync.SameThread(() =>
                {
                    if (assigning)
                        throw new RecursiveReactionException();
                    assigning = true;
                    try
                    {
                        state.Assign(x);
                    }
                    finally
                    {
                        assigning = false;
                    }
                });

                var valueSubscription = value.ActivateDerived(o, x =>
                {
                    assign(x);
                    o.OnValue(x);
                });

                return delegate
                {
                    targetSubscription.Dispose();
                    valueSubscription.Dispose();
                    if (hasExternal)
                        assign(lastExternal);
                };
            });
        }
        public static IExpression<TValue> Assign<TTarget, TValue>(this IExpression<TTarget> target, Func<TTarget, IAssignableExpression<TValue>> stateSelector, IExpression<TValue> value)
        {
            Contract.Requires<ArgumentNullException>(target != null, "target");
            Contract.Requires<ArgumentNullException>(stateSelector != null, "stateSelector");
            Contract.Requires<ArgumentNullException>(value != null, "value");

            return Create<TValue>(o =>
            {
                var momizedValue = value.Memoize();
                var assignHandle = Disposable.Empty;
                var targetHandle = target.ActivateDerived(o, t =>
                {
                    assignHandle.Dispose();
                    var state = stateSelector(t);
                    if (state == null)
                        throw new InvalidOperationException("stateSelector returned null");
                    assignHandle = Assign(state, momizedValue).Activate(o);
                });
                return delegate
                {
                    assignHandle.Dispose();
                    targetHandle.Dispose();
                };
            });
        }

        public static IExpression<T> If<T>(this IExpression<bool> test, IExpression<T> trueValue, IExpression<T> falseValue)
        {
            Contract.Requires<ArgumentNullException>(test != null, "test");
            Contract.Requires<ArgumentNullException>(trueValue != null, "trueValue");
            Contract.Requires<ArgumentNullException>(falseValue != null, "falseValue");
            return test.Distinct().Apply(x => x ? trueValue : falseValue);
        }
        internal static IExpression<T> If<T>(this IExpression<T> value, IExpression<bool> condition)
        {
            Contract.Requires<ArgumentNullException>(condition != null, "test");
            Contract.Requires<ArgumentNullException>(value != null, "trueValue");
            return If(condition, value, Empty<T>());
        }

        public static IExpression<TReturn> Call<TReturn>(this IStatement<TReturn> methodBody)
        {
            Contract.Requires<ArgumentNullException>(methodBody != null, "methodBody");
            return Expression.CreateWithDisposable<TReturn>(o =>
            {
                Func<string, Func<IDisposable>> exitFor = stmt => () => { throw new InvalidStatementUsageException("A {0} statement is not enclosed by a foreach loop".InvariantFormat(stmt)); };

                return methodBody.Activate(
                    onException: o.OnException,
                    onReturn: x => x.Activate(o),
                    onContinue: exitFor("continue"),
                    onBreak: exitFor("break")
                );
            });
        }
    }
}